package com.qd.system.service.person;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cdqidi.constant.ConstantDto;
import com.cdqidi.dto.ApiResult;
import com.cdqidi.dto.ApiResultWrapper;
import com.cdqidi.dto.CacheKey;
import com.cdqidi.exception.ApiException;
import com.cdqidi.util.*;
import com.cdqidi.util.page.PageUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qd.common.sys.constant.Constants;
import com.qd.common.sys.domain.entity.login.LoginUserDTO;
import com.qd.common.sys.domain.entity.login.LoginUserDisplay;
import com.qd.common.sys.domain.entity.person.PersonDTO;
import com.qd.common.sys.domain.login.LoginStatusDTO;
import com.qd.common.sys.domain.login.LoginUser;
import com.qd.common.sys.util.SecurityUtils;
import com.qd.common.sys.util.UserAgent;
import com.qd.system.service.dict.MbUtil;
import com.qd.system.service.login.LoginUserService;
import com.qd.system.service.org.OrgService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * token验证处理
 *
 * @author sjk
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class AuthTokenService {
    private static final String OFFICIAL = "official";
    private final RedisTemplateUtil<LoginUser> redisTemplateUtil;
    private final StringRedisTemplate stringRedisTemplate;
    private final LoginUserService loginUserService;
    private final PersonService personService;
    private final ObjectMapper objectMapper;
    /**
     * 令牌自定义标识
     */
    @Value("${token.header}")
    private String header;
    /**
     * 令牌秘钥
     */
    @Value("${token.secret}")
    private String secret;
    /**
     * 令牌有效期（默认30分钟）
     */
    @Value("${token.expireTime}")
    private int expireTime;

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        final String token = getToken(request);
        if (StringUtils.hasLength(token) && token.length() > 64) {
            final Claims claims = parseToken(token);
            // 解析对应的权限以及用户信息
            final String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
            final String userKey = getTokenKey(uuid);
            return redisTemplateUtil.get(userKey, LoginUser.class);
        }
        return null;
    }

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser) {
        final String token = IdWorker.getIdStr();
        loginUser.setToken(token);
        refreshToken(loginUser);
        final Map<String, Object> claims = new HashMap<>(1);
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

    /**
     * 验证令牌有效期，相差不足20分钟，自动刷新缓存
     *
     * @param loginUser 登录用户
     */
    public void verifyToken(LoginUser loginUser) {
        final long currentTime = System.currentTimeMillis();
        final long millisMinuteTwe = 20 * ConstantDto.REDIS_TIME_OUT_MIN;
        if (loginUser.getExpireTime() - currentTime <= millisMinuteTwe) {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser) {
        final long time = expireTime * ConstantDto.REDIS_TIME_OUT_MIN;
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + time);
        // 根据uuid将loginUser缓存
        final String userKey = getTokenKey(loginUser.getToken());
        final String tokenKey = getUserIdKey(loginUser.getUser().getPersonId(), loginUser.getRemote());
        final String activeProfile = SpringContextHolder.getActiveProfile();
        if (StringUtils.hasLength(activeProfile) && OFFICIAL.equals(activeProfile)) {
            String oldToken = stringRedisTemplate.opsForValue().get(tokenKey);
            clearToken(oldToken);
        }
        //获得tokenKey的时候已经加了前缀，此处无需重复加
        redisTemplateUtil.set(false, tokenKey, userKey, time, TimeUnit.MILLISECONDS);
        String key = RedisTemplateUtil.buildKey(userKey, true);
        redisTemplateUtil.set(new CacheKey().setKey(key).setTimeout(time), loginUser);
    }

    public void refreshLoginUser(LoginUser loginUser) {
        if (log.isDebugEnabled()) {
            log.debug("refreshLoginUser:{}", loginUser.getUser().getAppUserExtend());
        }
        final long time = expireTime * ConstantDto.REDIS_TIME_OUT_MIN;
        // 根据uuid将loginUser缓存
        final String userKey = getTokenKey(loginUser.getToken());
        redisTemplateUtil.set(new CacheKey().setKey(userKey).setTimeout(time), loginUser);
    }

    /**
     * 刷新loginUser中的AppUserExtend对象
     *
     * @param loginUser 登录用户
     */
    public void refreshAppUserExtend(LoginUser loginUser) {
        // 根据uuid将loginUser缓存
        final String userKey = getTokenKey(loginUser.getToken());
        final long time = expireTime * ConstantDto.REDIS_TIME_OUT_MIN;
        redisTemplateUtil.set(new CacheKey().setKey(userKey).setTimeout(time), loginUser);
    }

    /**
     * 删除用户登录信息
     *
     * @param personId 用户id
     */
    public void clearLoginCache(String personId) {
        final String key = getUserIdKey(personId, "*");
        final Set<String> keys = stringRedisTemplate.keys(key);
        String token;
        assert keys != null;
        for (String s : keys) {
            token = stringRedisTemplate.opsForValue().get(s);
            clearToken(token);
            stringRedisTemplate.delete(s);
        }

    }

    /**
     * 判断是否之前登陆过，清空之前的登录信息，保证一个用户+remote只登录一次
     *
     * @param personId 用户ID
     * @param remote   登录类型
     */
    public void clearLoginCache(String personId, String remote) {
        final String key = getUserIdKey(personId, remote);
        stringRedisTemplate.delete(key);
    }

    public void loginOut() {
        final String token = getToken(ServletUtil.getRequest());
        if (StringUtils.hasLength(token)) {
            final String uid = getTokenToUid(token);
            if (StringUtils.hasLength(uid)) {
                clearToken(uid);
            }
        }
    }

    /**
     * 根据token删除用户登录信息
     *
     * @param token 登录令牌
     */
    public void clearToken(String token) {
        if (StringUtils.hasLength(token)) {
            final String tokenKey = RedisTemplateUtil.buildKey(getTokenKey(token), true);
            final LoginUser user = redisTemplateUtil.get(false, tokenKey, LoginUser.class);
            if (null != user) {
                clearLoginCache(user.getUser().getPersonId(), user.getRemote());
            }
            redisTemplateUtil.deleteByKey(tokenKey);
        }
    }

    /**
     * 设置用户代理信息
     *
     * @param loginUser 登录信息
     */
    public void setUserAgent(LoginUser loginUser) {
        final String userAgentString = ServletUtil.getRequest().getHeader("User-Agent");
        final UserAgent userAgent = UserAgent.parseUserAgentString(userAgentString);
        final String ip = IpHelper.getIpAddress(ServletUtil.getRequest());
        loginUser.setBrowser(userAgent.getBrowser());
        loginUser.setIpaddr(ip);
        loginUser.setOs(userAgent.getOperatingSystem());

    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims) {
        SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        return Jwts.builder().claims(claims).signWith(secretKey, Jwts.SIG.HS512).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token) {
//        log.info("token:{}",token);
        SecretKey secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        final Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     *
     * @param request 请求
     * @return token
     */
    public String getToken(HttpServletRequest request) {
        String token = request.getHeader(header);
        if (!StringUtils.hasLength(token)) {
            token = request.getParameter(header);
        }
        if (StringUtils.hasLength(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    private String getTokenKey(String uuid) {
        if (uuid.startsWith(Constants.LOGIN_TOKEN_KEY)) {
            return uuid;
        }
        return Constants.LOGIN_TOKEN_KEY + uuid;
    }

    public String getUserIdKey(String userId, String remote) {
        return RedisTemplateUtil.getRedisCacheKey(Constants.LOGIN_USERID_KEY, userId, remote);
    }

    public String getTokenToUid(String token) {
        final Claims claims = parseToken(token);
        // 解析对应的权限以及用户信息
        return (String) claims.get(Constants.LOGIN_USER_KEY);
    }

    /**
     * 更新用户登录状态
     *
     * @param dto 登录状态
     * @return 成功 or 失败
     */
    @Transactional(rollbackFor = Exception.class)
    public ApiResultWrapper<Object> updateLoginUserState(LoginStatusDTO dto) {
        updateLoginUser(dto);
        updatePersonService(dto);
        return ApiResult.success("操作成功");
    }

    private void updateLoginUser(LoginStatusDTO dto) {
        final String personId = dto.getPersonId();
        final Integer loginStatus = dto.getLoginStatus();
        if (!MbUtil.isExists(loginStatus)) {
            throw new ApiException("用户登录状态值错误[" + loginStatus + "]");
        }
        final com.qd.system.model.LoginUser loginUser = loginUserService.getById(personId);
        if (null == loginUser) {
            throw new ApiException("用户不存在[" + personId + "]");
        }
        if (null != loginUser.getState() && loginUser.getState().equals(loginStatus)) {
            throw new ApiException("用户状态无需修改");
        }
        if (OrgValidService.isOperatorByOrgId(loginUser.getOrgId())) {
            throw new ApiException("没有权限修改用户登录状态");
        }
        loginUserService.clearCache(loginUser);
        clearLoginCache(dto.getPersonId());
        com.qd.system.model.LoginUser updateLoginUser = new com.qd.system.model.LoginUser();
        updateLoginUser.setPersonId(dto.getPersonId());
        updateLoginUser.setState(dto.getLoginStatus());
        loginUserService.updateById(updateLoginUser);
        updateLoginUser.freeData();
    }

    private void updatePersonService(LoginStatusDTO dto) {
        PersonDTO personDTO = personService.getByIdDto(dto.getPersonId());
        if (!personDTO.getUserStatus().equals(dto.getLoginStatus())) {
            personDTO.setUpdateTime(LocalDateTime.now());
            personDTO.setUserStatus(dto.getLoginStatus());
            personService.update(personDTO);
            personDTO.freeData();
        }
    }


    public Page<LoginUserDisplay> userTokenPage(Page<LoginUserDisplay> page, LoginUserDisplay loginUserDisplay) {
        long current = page.getCurrent();
        long size = (int) page.getSize();
        List<LoginUserDisplay> dataList = new ArrayList<>();
        final List<String> keyList = new ArrayList<>(redisTemplateUtil.scan(Constants.LOGIN_TOKEN_KEY));
        boolean isParam = isParam(loginUserDisplay);
        int total = keyList.size();
        page.setTotal(total);
        page.setSearchCount(false);
        if (isParam) {
            search(keyList, page, loginUserDisplay, dataList);
            page.setTotal(dataList.size());
            page.setCurrent(current);
            page.setSize(size);
        } else {
            search(keyList, page, dataList);
        }
        if (!dataList.isEmpty()) {
            if (dataList.size() > size) {
                List<LoginUserDisplay> subList = PageUtil.page(dataList, dataList.size(), (int) current, (int) size);
                page.setRecords(subList);
            } else {
                page.setRecords(dataList);
            }
        }
        return page;
    }

    public void search(List<String> keyList, Page<LoginUserDisplay> page, List<LoginUserDisplay> dataList) {
        List<String> pageKey = PageUtil.page(keyList, (int) page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
        List<String> jsonStr = stringRedisTemplate.opsForValue().multiGet(pageKey);
        if (null == jsonStr || jsonStr.isEmpty()) {
            return;
        }
        List<LoginUserDisplay> list = jsonStr.stream().map(str -> {
            try {
                return objectMapper.readValue(str, LoginUser.class);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            return null;
        }).filter(Objects::nonNull).map(this::buildLoginUserDisplay).collect(Collectors.toList());
        if (!list.isEmpty()) {
            dataList.addAll(list);
        }
    }

    /**
     * 带查询条件的必须迭代完成全部数据，因为要取total
     *
     * @param keyList          redis中key的集合
     * @param page             分页对象
     * @param loginUserDisplay 查询对象
     * @param dataList         存储结果的对象
     */
    public void search(List<String> keyList, Page<LoginUserDisplay> page, LoginUserDisplay loginUserDisplay, List<LoginUserDisplay> dataList) {
        List<String> pageKey = PageUtil.page(keyList, (int) page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
        List<String> jsonStr = stringRedisTemplate.opsForValue().multiGet(pageKey);
        if (null == jsonStr || jsonStr.isEmpty()) {
            return;
        }
        String account = loginUserDisplay.getAccount();
        String personName = loginUserDisplay.getPersonName();
        String currentOrgId = loginUserDisplay.getCurrentOrgId();
        String idCard = loginUserDisplay.getIdCard();
        String mobile = loginUserDisplay.getMobile();

        List<LoginUser> list = getList(jsonStr, account, personName, currentOrgId, idCard, mobile);
        if (!list.isEmpty()) {
            dataList.addAll(list.stream().map(this::buildLoginUserDisplay).collect(Collectors.toList()));
        }
        if (!page.hasNext()) {
            return;
        }
        page.setCurrent(page.getCurrent() + 1);
        search(keyList, page, loginUserDisplay, dataList);
    }

    private LoginUserDisplay buildLoginUserDisplay(LoginUser loginUser) {
        final LoginUserDisplay userDisplay = new LoginUserDisplay();
        userDisplay.setTokenId(Constants.LOGIN_TOKEN_KEY + loginUser.getToken());
        userDisplay.setToken(loginUser.getToken());
        userDisplay.setPersonId(loginUser.getUserId());
        userDisplay.setCurrentOrgId(loginUser.getCurrentOrgId());
        userDisplay.setCurrentOrgIdDisplay(SpringContextHolder.getBean(OrgService.class).idToDisplay(loginUser.getCurrentOrgId()));
        userDisplay.setLoginTime(DateUtil.longToLocalDateTime(loginUser.getLoginTime()));
        userDisplay.setExpireTime(DateUtil.longToLocalDateTime(loginUser.getExpireTime()));
        userDisplay.setIpaddr(loginUser.getIpaddr());
        userDisplay.setLoginLocation(loginUser.getLoginLocation());
        userDisplay.setBrowser(loginUser.getBrowser());
        userDisplay.setOs(loginUser.getOs());
        userDisplay.setRemote(loginUser.getRemote());
        userDisplay.setRemoteDisplay(MbUtil.idToDisplay(loginUser.getRemote()));
        final LoginUserDTO user = loginUser.getUser();
        if (null != user) {
            userDisplay.setAccount(user.getAccount());
            userDisplay.setPersonName(user.getPersonName());
            userDisplay.setIdCard(user.getIdCard());
            userDisplay.setMobile(user.getMobile());
            userDisplay.setSex(user.getSex());
            userDisplay.setSexDisplay(user.getSexDisplay());
        }
        return userDisplay;
    }

    private boolean isParam(LoginUserDisplay loginUserDisplay) {
        if (StringUtils.hasLength(loginUserDisplay.getAccount())) {
            return true;
        }
        if (StringUtils.hasLength(loginUserDisplay.getPersonName())) {
            return true;
        }
        if (StringUtils.hasLength(loginUserDisplay.getCurrentOrgIdDisplay())) {
            return true;
        }
        if (StringUtils.hasLength(loginUserDisplay.getIdCard())) {
            return true;
        }
        return StringUtils.hasLength(loginUserDisplay.getMobile());
    }

    private List<LoginUser> getList(List<String> jsonStr, String account, String personName, String currentOrgId, String idCard, String mobile) {
        Stream<LoginUser> stream = jsonStr.stream().map(str -> {
            try {
                return objectMapper.readValue(str, LoginUser.class);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            return null;
        });
        return stream.filter(loginUser -> {
            boolean b = true;
            if (StringUtils.hasLength(account)) {
                b = loginUser.getUser().getAccount().equals(account);
            }
            return b;
        }).filter(loginUser -> {
            boolean b = true;
            if (StringUtils.hasLength(personName)) {
                b = loginUser.getUser().getPersonName().equals(personName);
            }
            return b;
        }).filter(loginUser -> {
            boolean b = true;
            if (StringUtils.hasLength(currentOrgId)) {
                b = loginUser.getCurrentOrgId().equals(currentOrgId);
            }
            return b;
        }).filter(loginUser -> {
            boolean b = true;
            if (StringUtils.hasLength(idCard)) {
                b = loginUser.getUser().getIdCard().equals(idCard);
            }
            return b;
        }).filter(loginUser -> {
            boolean b = true;
            if (StringUtils.hasLength(mobile)) {
                b = loginUser.getUser().getMobile().equals(mobile);
            }
            return b;
        }).collect(Collectors.toList());
    }

    public LoginUserDisplay userTokenDetail(String tokenId) {
        final String s = stringRedisTemplate.opsForValue().get(tokenId);
        try {
            if (StringUtils.hasLength(s)) {
                final LoginUser loginUser = objectMapper.readValue(s, LoginUser.class);
                if (null != loginUser) {
                    return buildLoginUserDisplay(loginUser);
                }
            } else {
                throw new ApiException("token不存在[" + tokenId + "]");
            }

        } catch (JsonProcessingException e) {
            throw new ApiException(e);
        }
        throw new ApiException("token不存在[" + tokenId + "]");
    }

    public void offlineLoginUser(List<String> ids) {
        ids.forEach(tokenId -> {
            String s = stringRedisTemplate.opsForValue().get(tokenId);
            if (!StringUtils.hasLength(s)) {
                stringRedisTemplate.delete(tokenId);
            } else {
                throw new ApiException("tokenId不存在[" + tokenId + "]");
            }
        });
    }

    /**
     * 更新缓存中的用户信息
     *
     * @param loginUser 登录信息
     */
    public void updateTokenCache(LoginUserDTO loginUser) {
        SecurityUtils.getLoginUser().setUser(loginUser);
        refreshToken(SecurityUtils.getLoginUser());
    }
}
